
//
// 130310 - AH - Created.
// 130314 - AH - get(String).
// 130316a- AH - Now extends HttpProtocolDef, name and desc property code
//               removed from here.
// 130316b- AH - parse(InputStream) and parse(File).
// 130317 - AH - compileApiHtml() and compileApiHtml(boolean).
//

package org.japril.servlet;

import java.io.CharArrayWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.japril.jutil.SaxUtil;
import org.xml.sax.Attributes;
import org.xml.sax.ContentHandler;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.helpers.DefaultHandler;

/**
 * <p>
 *   Collection of
 *   {@link HttpRequestDef} that makes up a protocol.
 * </p>
 * @author Arne Halvorsen (AH)
 */
public final class HttpProtocolDef extends HttpProtocolElement 
  implements Iterable<HttpRequestDef> 
{
  private final List<HttpRequestDef> m_requests=new ArrayList<HttpRequestDef>();
  
  private final Map<String, HttpRequestDef> m_map=
    new HashMap<String, HttpRequestDef>();
  
  /**
   * <p>
   *   Constructor.
   * </p>
   * @param name Name of protocol.
   * @throws IllegalArgumentException If {@code name} is the empty string.
   */
  public HttpProtocolDef(String name){ super(name); }
  
  /**
   * <p>
   *   Adds
   *   {@link HttpProtocolDef}.
   * </p>
   * @param def Definition to add.
   * @return {@code this}.
   * @throws IllegalArgumentException If request defined for action.
   */
  public HttpProtocolDef addHttpRequestDef(HttpRequestDef def)
  {
    if (def==null)
    {
      throw new NullPointerException("def");
    }
    
    String action=def.action();
    if (m_map.containsKey(action))
    {
      throw new IllegalArgumentException("request defined for action: "+action);
    }
    
    m_requests.add(def);
    m_map.put(action, def);
    return this;
  }
  
  /**
   * <p>
   *   Removes
   *   {@link HttpProtocolDef}.
   * </p>
   * @param def Definition to remove.
   * @return {@code true} if removed, {@code false} if failed.
   */
  public boolean removeHttpRequestDef(HttpRequestDef def)
  {
    if (def==null)
    {
      throw new NullPointerException("def");
    }
    
    String action=def.action();
    if (m_map.containsKey(action))
    {
      m_map.remove(action);
      m_requests.remove(def);
      return true;
    }
    
    return false;
  }
  
  /**
   * <p>
   *   Gets number of
   *   {@link HttpRequestDef}s.
   * </p>
   * @return Count.
   */
  public int size(){ return m_requests.size(); }
  
  /**
   * <p>
   *   Tells if {@code this} has no requests definitions.
   * </p>
   * @return {@code size()==0}.
   */
  public boolean isEmpty(){ return size()==0; }
  
  /**
   * <p>
   *   Gets 
   *   {@link HttpProtocolDef}.   
   * </p>
   * @param action {@link HttpRequestDef#action() Action} defined request is 
   *               for.
   * @return Definition or {@code null}.
   */
  public HttpRequestDef get(String action)
  {
    if (action==null)
    {
      throw new NullPointerException("action");
    }
    
    return m_map.get(action);
  }
  
  /**
   * <p>
   *   Gets 
   *   {@link HttpProtocolDef}.
   * </p>
   * @param idx Index of definition to get.
   * @return {@link HttpRequestDef}.
   * @throws IndexOutOfBoundsException If {@code idx<0 || idx>=size()}.
   */
  public HttpRequestDef get(int idx){ return m_requests.get(idx); }
  
  /**
   * <p>
   *   Gets index of definition.
   * </p>
   * @param def {@link HttpRequestDef} to get index of.
   * @return Index or {@code -1} if not found.
   */
  public int indexOf(HttpRequestDef def){ return m_requests.indexOf(def); }
  
  
  // Specified in Iterable<HttpRequestDef>.
  public Iterator<HttpRequestDef> iterator(){ return m_requests.iterator(); }
  
  /**
   * <p>
   *   Compiles standalone HTML that describes protocol.
   * </p>
   * @return HTML.
   * @see #compileApiHtml(boolean)
   */
  public String compileApiHtml(){ return compileApiHtml(true); }
  
  /**
   * <p>
   *   Compiles HTML that describes protocol.
   * </p>
   * @param standalone {@code true} if to create standalone HTML, {@code false}
   *                   if to create HTML to be included in other.
   * @return HTML.
   * @see #compileApiHtml() 
   */
  public String compileApiHtml(boolean standalone)
  {
    CharArrayWriter caw=new CharArrayWriter();
    PrintWriter pw=new PrintWriter(caw);
    
    if (standalone)
    {
      pw.println("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd\">");
      pw.println("<html><body>");      
    }
    
    pw.println("<h1>Protocol: "+name()+"</h1>");
    
    for (HttpRequestDef r : this)
    {
      pw.println(r.compileApiHtml());  
    }
    
    if (standalone)
    {
      pw.println("</body></html>");      
    }
    
    pw.flush();
    return caw.toString();
  }

  /**
   * <p>
   *   Gets XML representation.
   * </p>
   * @return XML.
   */
  public String toXml(){ return toXml(true, ""); }
  
  /**
   * <p>
   *   Gets XML representation.
   * </p>
   * @param standalone If to get standalone XML.
   * @return XML.
   */
  public String toXml(boolean standalone){ return toXml(standalone, ""); }
  
  /**
   * <p>
   *   Gets XML representation.
   * </p>
   * @param standalone If to get standalone XML.
   * @param indent     Indent to start at.
   * @return XML.
   */
  public String toXml(boolean standalone, String indent)
  {
    CharArrayWriter caw=new CharArrayWriter();
    PrintWriter pw=new PrintWriter(caw);
    
    StringBuilder sb=new StringBuilder();
    
    if (standalone)
    {
      sb.setLength(0);
      sb.append(indent).append("<?xml version='1.0'?>");
      pw.println(sb);
    }
    
    sb.setLength(0);
    sb.append(indent).append("<http-protocol name='").append(name())
      .append("'>");
    pw.println(sb);
  
    String desc=description();
    if (desc!=null)
    {
      sb.setLength(0);
      sb.append(indent).append("  <desc>").append(desc).append("</desc>");
      pw.println(sb);
    }
    
    if (!m_requests.isEmpty())
    {
      sb.setLength(0);
      sb.append(indent).append("  <http-requests>");
      pw.println(sb);
      
      for (HttpRequestDef def : m_requests)
      {
        String dxml=def.toXml(false, indent+"    ");
        pw.println(dxml);
      }
      
      sb.setLength(0);
      sb.append(indent).append("  </http-requests>");
      pw.println(sb);
    }
    
    sb.setLength(0);
    sb.append(indent).append("</http-protocol>");
    pw.println(sb);
    
    pw.flush();
    return caw.toString();
  }
  
  @Override
  public String toString()
  {
    CharArrayWriter caw=new CharArrayWriter();
    PrintWriter pw=new PrintWriter(caw);
    
    pw.println("Protocol: "+name());
    pw.println();
    
    pw.println("Description:");
    pw.println();
    String desc=description();
    if (desc!=null) 
    {
      pw.println(desc);
      pw.println();
    }
    
    pw.println("Requests:");
    pw.println();
    
    for (HttpRequestDef d : m_requests) pw.println(d);
    
    pw.flush();
    return caw.toString();
  }
  
  /**
   * <p>
   *   Parse XML produced by
   *   {@link #toXml()}.
   * </p>
   * @param f {@link File} to read XML from.
   * @return Result in form of a
   *         {@link HttpProtocolDef} object.
   * @throws IOException If fails to read or parse.
   */
  public static HttpProtocolDef parse(File f) throws IOException
  {
    if (f==null)
    {
      throw new NullPointerException("f");
    }
    
    InputStream is=null;
    try
    {
      is=new FileInputStream(f);
      return parse(is);
    }
    finally
    {
      if (is!=null) try { is.close(); } catch (IOException _){}
    }
  }
  
  /**
   * <p>
   *   {@link ContentHandler} to parse 
   *   {@link HttpProtocolDef#toXml() XML}.
   * </p>
   * @author Arne Halvorsen (AH)
   */
  private static final class H extends DefaultHandler
  {
    public HttpProtocolDef m_result=null;
    
    private HttpRequestDef m_req=null;
    
    private HttpRequestParamDef m_param=null;
    
    private Stack<HttpProtocolElement> m_stack=new Stack<HttpProtocolElement>();
    
    private Locator m_loc=null;
    
    /**
     * <p>
     *   Constructor.
     * </p>
     */
    H(){}
    
    // Specified in ContentHandler. 
    public void setDocumentLocator(Locator loc){ m_loc=loc; }

    // Specified in ContentHandler.
    public void startElement(String uri, String lName, String qName,
      Attributes attrs) throws SAXException 
    {
      String n=qName;
      
           if (n.equals("http-protocol"))          handleProtocolStart(attrs);
      else if (n.equals("http-requests"))          ;
      else if (n.equals("http-request"))           handleRequestStart(attrs);
      else if (n.equals("http-request-parameter")) handleRequestParStart(attrs);     
      else if (n.equals("desc"))                   ;
      else if (n.equals("req-info"))               ;
      else if (n.equals("par-info"))               ;     
      else if (n.equals("line"))                   ;
      else if (n.equals("options"))                ;
      else if (n.equals("option"))                 handleOptionStart(attrs);
      else
      {
        throw new SAXParseException("uknown element: "+n, m_loc);
      }
    }
    
    private final StringBuilder m_sb=new StringBuilder();
    
    // Specified in ContentHandler.
    public void characters(char[] ch, int start, int length)
      throws SAXException 
    {
      m_sb.append(ch, start, length);
    }

    private void handleProtocolStart(Attributes attrs) throws SAXException
    {
      String name=SaxUtil.getMandatoryAttr("name", attrs, m_loc);
      m_result=new HttpProtocolDef(name);
      m_stack.push(m_result);
    }
    
    private void handleRequestStart(Attributes attrs) throws SAXException
    {
      SaxUtil.assumeNotNull("http-request", m_result, m_loc);
      
      String name=SaxUtil.getMandatoryAttr("name", attrs, m_loc);
      String action=SaxUtil.getMandatoryAttr("action", attrs, m_loc);
      boolean post=SaxUtil.getMandatoryBooleanAttr("post", attrs, m_loc);
      HttpRequestDef req=new HttpRequestDef(post, action);
      req.name(name);
      
      String enctype=attrs.getValue("enctype");
      if (enctype!=null) req.enctype(enctype);
      
      m_req=req;
      m_result.addHttpRequestDef(req);
      m_stack.push(req);
    }
    
    private void handleRequestParStart(Attributes attrs) throws SAXException
    {
      SaxUtil.assumeNotNull("http-request-parameter", m_req, m_loc);
      
      String name=SaxUtil.getMandatoryAttr("name", attrs, m_loc);
      String cname=SaxUtil.getMandatoryAttr("class", attrs, m_loc);
      boolean mandatory=SaxUtil.getMandatoryBooleanAttr("mandatory", attrs,
        m_loc);
      String ftype=SaxUtil.getMandatoryAttr("ftype", attrs, m_loc);
      
      try
      {
        HttpRequestParamDef par=new HttpRequestParamDef(name);
        par.type(getParType(cname));
        par.mandatory(mandatory);
        par.ftype(ftype);
        
        String fvalue=attrs.getValue("fvalue");
        if (fvalue!=null) par.fvalue(fvalue);
        
        int size=SaxUtil.getIntAttr("size", -1, attrs, m_loc);
        if (size!=-1) par.size(size);
        
        int rows=SaxUtil.getIntAttr("rows", -1, attrs, m_loc);
        if (rows!=-1) par.rows(rows);
        
        int cols=SaxUtil.getIntAttr("cols", -1, attrs, m_loc);
        if (cols!=-1) par.cols(cols);
        
        m_req.parameter(par);
        m_param=par;
        m_stack.push(par);
      }
      catch (ClassNotFoundException cnfx)
      {
        throw new SAXParseException("type "+cname+" not found", m_loc);
      }
    }
    
    private static Class<?> getParType(String cname) 
      throws ClassNotFoundException
    {
           if (cname.equals("int"))     return Integer.TYPE;
      else if (cname.equals("long"))    return Long.TYPE;
      else if (cname.equals("boolean")) return Boolean.TYPE;
      else
      {
        return Class.forName(cname);
      }
    }
    
    public void handleOptionStart(Attributes attrs) throws SAXException
    {
      SaxUtil.assumeNotNull("option", m_param, m_loc);
      
      String value=SaxUtil.getMandatoryAttr("option", attrs, m_loc);
      m_param.option(value);
    }
    
    // Specified in ContentHandler.
    public void endElement(String uri, String lName, String qName)
      throws SAXException
    {
      String n=qName;
           if (n.equals("desc"))                   handleDescEnd();
      else if (n.equals("line"))                   handleLineEnd();
      else if (n.equals("par-info"))               handleParInfoEnd();
      else if (n.equals("http-protocol"))          handleProtocolElementEnd(); 
      else if (n.equals("http-request"))           handleProtocolElementEnd();
      else if (n.equals("http-request-parameter")) handleProtocolElementEnd();     
    }
    
    private void handleDescEnd() throws SAXException
    {
      HttpProtocolElement pe=(m_stack.isEmpty()) ? null : m_stack.peek();
      SaxUtil.assumeNotNull("desc", pe, m_loc);
      pe.description(m_sb.toString());
      m_sb.setLength(0);
    }
    
    private void handleLineEnd() throws SAXException
    {
      SaxUtil.assumeNotNull("line", m_req, m_loc);
      m_req.infoLine(m_sb.toString());
      m_sb.setLength(0);
    }
    
    private void handleParInfoEnd() throws SAXException
    {
      SaxUtil.assumeNotNull("par-info", m_param, m_loc);
      m_param.info(m_sb.toString());
      m_sb.setLength(0);
    }
    
    private void handleProtocolElementEnd()
    {
      m_stack.pop();
    }
    
  }
  
  /**
   * <p>
   *   Parse XML produced by
   *   {@link #toXml()}.
   * </p>
   * @param is Stream to read XML from.
   * @return Result in form of a
   *         {@link HttpProtocolDef} object.
   * @throws IOException If fails to read or parse.
   */
  public static HttpProtocolDef parse(InputStream is) throws IOException
  {
    if (is==null)
    {
      throw new NullPointerException("is");
    }
    
    try
    {
      SAXParserFactory f=SAXParserFactory.newInstance();
      f.setNamespaceAware(false);
      f.setValidating(false);
      SAXParser p=f.newSAXParser();
      
      H h=new H();
      p.parse(is, h);
      return h.m_result;
    }
    catch (SAXException saxx)
    {
      IOException iox=new IOException();
      iox.initCause(saxx);
      throw iox;
    }
    catch (ParserConfigurationException pcx)
    {
      IOException iox=new IOException();
      iox.initCause(pcx);
      throw iox;
    }
  }
  
}
